////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.charts
{

import flash.events.Event;
import mx.charts.chartClasses.AxisBase;
import mx.charts.chartClasses.AxisLabelSet;
import mx.charts.chartClasses.IAxis;
import mx.collections.ArrayCollection;
import mx.collections.CursorBookmark;
import mx.collections.ICollectionView;
import mx.collections.IViewCursor;
import mx.collections.XMLListCollection;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;

/**
 *  The CategoryAxis class lets charts represent data
 *  grouped by a set of discrete values along an axis.
 *  You typically use the CategoryAxis class to define
 *  a set of labels that appear along an axis of a chart.
 *  For example, charts that render data according to City,
 *  Year, Business unit, and so on.
 *  
 *  <p>A CategoryAxis used in a chart does not inherit its
 *  <code>dataProvider</code> property from the containing chart.
 *  You must explicitly set the <code>dataProvider</code> property
 *  on a CategoryAxis.</p>
 *  
 *  <p>While you can use the same dataProvider to provide data
 *  to the chart and categories to the CategoryAxis, a CategoryAxis
 *  can optimize rendering if its dataProvider is relatively static.
 *  If possible, ensure that the categories are relatively static
 *  and that changing data is stored in separate dataProviders.</p>
 *  
 *  <p>The <code>dataProvider</code> property can accept
 *  either an array of strings or an array of records (objects)
 *  with a property specifying the category name.
 *  If you specify a <code>categoryField</code> property,
 *  the CategoryAxis assumes the dataProvider is an array of Objects.
 *  If <code>categoryField</code> is <code>null</code>,
 *  the CategoryAxis assumes dataProvider is an array of Strings.</p>
 *  
 *  @mxml
 *  
 *  <p>The <code>&lt;mx:CategoryAxis&gt;</code> tag inherits all the properties
 *  of its parent classes and adds the following properties:</p>
 *  
 *  <pre>
 *  &lt;mx:CategoryAxis
 *    <strong>Properties</strong>
 *    categoryField="null"
 *    dataProvider="<i>No default</i>"
 *    labelFunction="<i>No default</i>"
 *    padding="<i>Default depends on chart type</i>"
 *    ticksBetweenLabels="<i>true</i>"
 *  /&gt;
 *  </pre>
 *
 *  @includeExample examples/HLOCChartExample.mxml
 */
public class CategoryAxis extends AxisBase implements IAxis
{
    include "../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------

	/**
	 *  Constructor.
	 */
	public function CategoryAxis()
	{
		super();

		workingDataProvider = new ArrayCollection();
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  @private
	 */
	private var _cursor:IViewCursor;
	
	/**
	 *  @private
	 */
	private var _catMap:Object;

	/**
	 *  @private
	 */
	private var _categoryValues:Array;
	
	/**
	 *  @private
	 */
	private var _labelsMatchToCategoryValuesByIndex:Array;

	/**
	 *  @private
	 */
	private var _cachedMinorTicks:Array = null;	
	
	/**
	 *  @private
	 */
	private var _cachedTicks:Array = null;	

	/**
	 *  @private
	 */
	private var _labelSet:AxisLabelSet;
	
	//--------------------------------------------------------------------------
	//
	//  Overridden properties
	//
	//--------------------------------------------------------------------------

    //----------------------------------
	//  chartDataProvider
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the chartDataProvider property.
	 */
	private var _chartDataProvider:Object;
	
	/**
	 *  @private
	 */
	override public function set chartDataProvider(value:Object):void
	{
		_chartDataProvider = value;

		if (!_userDataProvider)
			workingDataProvider = _chartDataProvider;
	}
	
	//--------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------
	
    //----------------------------------
	//  baseline
    //----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  @inheritDoc
	 */
	public function get baseline():Number
	{
		return -_padding;
	}

    //----------------------------------
	//  categoryField
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the categoryField property.
	 */
	private var _categoryField:String = "";
	
    [Inspectable(category="General")]
	
	/**
	 *  Specifies the field of the data provider
	 *  containing the text for the labels.
	 *  If this property is <code>null</code>, CategoryAxis assumes 
	 *  that the dataProvider contains an array of Strings.
	 *
	 *  @default null
	 */
	public function get categoryField():String
	{
		return _categoryField;
	}
	
	/**
	 *  @private
	 */
	public function set categoryField(value:String):void
	{
		_categoryField = value;

		collectionChangeHandler();
	}

    //----------------------------------
	//  dataProvider
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the dataProvider property.
	 */
	private var _dataProvider:ICollectionView;
	
	/**
	 *  @private
	 */
	private var _userDataProvider:Object;
	
	[Inspectable(category="General")]
	
	/**
	 *  Specifies the data source containing the label names.
	 *  The <code>dataProvider</code> can be an Array of Strings, an Array of Objects,
	 *  or any object that implements the IList or ICollectionView interface.
	 *  If the <code>dataProvider</code> is an Array of Strings,
	 *  ensure that the <code>categoryField</code> property
	 *  is set to <code>null</code>. 
	 *  If the dataProvider is an Array of Objects,
	 *  set the <code>categoryField</code> property
	 *  to the name of the field that contains the label text.
	 */
	public function get dataProvider():Object
	{
		return _dataProvider;
	}
	
	/**
	 *  @private
	 */
	public function set dataProvider(value:Object):void
	{
		_userDataProvider = value;

		if (_userDataProvider != null)
			workingDataProvider = _userDataProvider;
		else
			workingDataProvider = _chartDataProvider;
	}

    //----------------------------------
	//  labelFunction
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the labelFunction property.
	 */
	private var _labelFunction:Function = null;
	
    [Inspectable(category="General")]
	
	/**
	 *  Specifies a function that defines the labels that are generated
	 *  for each item in the categoryAxis <code>dataProvider</code>.
	 *  If no <code>labelFunction</code> is provided,
	 *  the axis labels default to the value of the category itself.
	 *
	 *  <p>The <code>labelFunction</code> for a CategoryAxis
	 *  must have the following signature:<br></br>
	 *    <code>labelFunction(<i>categoryValue</i>:Object, <i>previousCategoryValue</i>:Object, 
	 *    <i>axis</i>:CategoryAxis, <i>categoryItem</i>:Object);</code></p>
	 *  
	 *  <p>where:<br></br>
	 *   <code><i>categoryValue</i></code> is the value of the category to be represented.<br></br>
	 *   <code><i>previousCategoryValue</i></code> is the value of the previous category on the axis.<br></br>
	 *   <code><i>axis</i></code> is the CategoryAxis being rendered.<br></br>
	 *   <code><i>categoryItem</i></code> is the item from the <code>dataProvider</code> that is being represented.
	 *  </p>
	 *  
	 *  <p>If the CategoryAxis.categoryField is not set, the value
	 *  will be the same as the <code>categoryValue</code> property.</p>
	 */	
	public function get labelFunction():Function
	{
		return _labelFunction;
	}
	
	/**
	 *  @private
	 */
	public function set labelFunction(value:Function):void
	{
		_labelFunction = value;

		invalidateCategories();
	}
	
    //----------------------------------
	//  minorTicks (private)
    //----------------------------------
 
 	/**
	 *  @private
	 */
	private function get minorTicks():Array
	{
		if (!_cachedMinorTicks)
		{
			_cachedMinorTicks = [];

			var n:int;
			var min:Number;
			var max:Number;
			var alen:Number;
			var i:Number;
			
			if (_ticksBetweenLabels == false)
			{
				n = _categoryValues.length;
				min = -_padding;
				max = n - 1 + _padding;
				alen = max - min;
				
				var start:Number = min <= -0.5 ? 0 : 1;
				var end:Number = max >= n - 0.5 ? n : n - 1
				
				for (i = start; i <= end; i++) // <= to draw final tick
				{
					_cachedMinorTicks.push((i - 0.5 - min) / alen);
				}
			}
			else
			{
				n = _categoryValues.length;
				min = -_padding;
				max = n - 1 + _padding;
				alen = max - min;
				
				for (i = 0; i < n; i++) // <= to draw final tick
				{
					_cachedMinorTicks.push((i - min) / alen);
				}
			}
		}

		return _cachedMinorTicks;
	}

    //----------------------------------
	//  padding
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the padding property.
	 */
	private var _padding:Number = 0.5;
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the padding added to either side of the axis
	 *  when rendering data on the screen.
	 *  Set to 0 to map the first category to the
	 *  very beginning of the axis and the last category to the end.
	 *  Set to 0.5 to leave padding of half the width
	 *  of a category on the axis between the beginning of the axis
	 *  and the first category and between the last category
	 *  and the end of the axis.
	 *  
	 *  <p>This is useful for chart types that render beyond the bounds
	 *  of the category, such as columns and bars.
	 *  However, when used as the horizontalAxis in a LineChart or AreaChart,
	 *  it is reset to 0.</p>
	 *  
	 *  @default 0.5
	 */
	public function get padding():Number
	{
		return _padding;
	}
	
	/**
	 *  @private
	 */
	public function set padding(value:Number):void
	{
		_padding = value;

		invalidateCategories();

		dispatchEvent(new Event("mappingChange"));
		dispatchEvent(new Event("axisChange"));
	}
	
    //----------------------------------
	//  ticksBetweenLabels
    //----------------------------------

	/**
	 *  @private
	 *  Storage for the tickBetweenLabels property.
	 */
	private var _ticksBetweenLabels:Boolean = true;
	
	[Inspectable]

	/**
	 *  Specifies the location of major tick marks on the axis,
	 *  relative to the category labels.
	 *  If <code>true</code>, tick marks (and any associated grid lines)
	 *  appear between the categories.
	 *  If <code>false</code>, tick marks appear in the middle of the category,
	 *  aligned with the label.
	 */
	public function get ticksBetweenLabels():Boolean
	{
		return _ticksBetweenLabels;
	}

	/**
	 *  @private
	 */
	public function set ticksBetweenLabels(value:Boolean):void
	{
		_ticksBetweenLabels = value;
	}

    //----------------------------------
	//  workingDataProvider
    //----------------------------------

	/**
	 *  @private
	 */
	private function set workingDataProvider(value:Object):void
	{
		if (_dataProvider != null)
		{
			_dataProvider.removeEventListener(
				CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);
		}
		
		if (value is Array)
		{
			value = new ArrayCollection(value as Array);
		}
		else if (value is ICollectionView)
		{
		}
		else if (value is XMLList)
		{
			value = new XMLListCollection(XMLList(value));
		}
		else if (value != null)
		{
			value = new ArrayCollection([ value ]);
		} 
		else 
		{
			value = new ArrayCollection();
		}
			
		_dataProvider = ICollectionView(value);

		_cursor = value.createCursor();

		if (_dataProvider != null) 
		{
			// weak listeners to collections and dataproviders
			_dataProvider.addEventListener(
				CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler, false, 0, true);
		}

		collectionChangeHandler();
	}
    
	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  @copy mx.charts.chartClasses.IAxis#mapCache()
	 */
	public function mapCache(cache:Array, field:String,
							 convertedField:String,
							 indexValues:Boolean = false):void
	{
		update();

		var n:int = cache.length;
		
		// Find the first non null item in the cache so we can determine type.
		// Since these initial values are null,
		// we can safely skip assigning values for them.
		for (var i:int = 0; i < n; i++)
		{
			if (cache[i][field] != null)
				break;
		}
		if (i == n)
			return;
		
		var value:Object = cache[i][field]
		if (value is XML ||
				 value is XMLList)
		{
			for (; i < n; i++)
			{
				cache[i][convertedField] = _catMap[cache[i][field].toString()];				
			}
		}
		else if ((value is Number || value is int || value is uint) &&
				 indexValues == true)
		{
			for (i = 0; i < n; i++)
			{
				var v:Object = cache[i];
				v[convertedField] = v[field];
			}
		}
		else
		{
			for (; i < n; i++)
			{
				cache[i][convertedField] = _catMap[cache[i][field]];				
			}
		}
		
	}

	/**
	 *  @copy mx.charts.chartClasses.IAxis#filterCache()
	 */
	public function filterCache(cache:Array, field:String,
								filteredField:String):void
	{
		update();

		// Our bounds are the categories, plus/minus padding,
		// plus a little fudge factor to account for floating point errors.
		var computedMaximum:Number = _categoryValues.length - 1 +
									 _padding + 0.000001;
		var computedMinimum:Number = -_padding - 0.000001;
		
		var n:int = cache.length;
		for (var i:int = 0; i < n; i++)
		{
			var v:Number =  cache[i][field];
			cache[i][filteredField] = v >= computedMinimum &&
									  v < computedMaximum ?
									  v :
									  NaN;
		}
	}

	/**
	 *  @copy mx.charts.chartClasses.IAxis#transformCache()
	 */
	public function transformCache(cache:Array, field:String,
								   convertedField:String):void
	{
		update();

		var min:Number = -_padding;
		var max:Number = _categoryValues.length - 1 + _padding;
		var alen:Number = max - min;

		var n:int = cache.length;
		for (var i:int = 0; i < n; i++)
		{
			cache[i][convertedField] = (cache[i][field] - min) / alen;
		}
	}

	/**
	 *  @copy mx.charts.chartClasses.IAxis#invertTransform()
	 */
	public function invertTransform(value:Number):Object
	{
		update();

		var min:Number = -_padding;
		var max:Number = _categoryValues.length - 1 + _padding;
		var alen:Number = max - min;

		return _categoryValues[Math.round((value * alen) + min)];
	}

    /**
     *  @copy mx.charts.chartClasses.IAxis#formatForScreen()
	 */
	public function formatForScreen(value:Object):String	
	{
		if (value is Number && value < _categoryValues.length)
		{
			var catValue:Object = _categoryValues[Math.round(Number(value))];
			return catValue == null ? value.toString() : catValue.toString();
		}

		return value.toString();	
	}

	/**
     *  @copy mx.charts.chartClasses.IAxis#getLabelEstimate()
	 */
	public function getLabelEstimate():AxisLabelSet
	{
		update();

		return _labelSet;
	}

	/**
     *  @copy mx.charts.chartClasses.IAxis#preferDropLabels()
	 */
	public function preferDropLabels():Boolean
	{
		return false;
	}

    /**
     *  @copy mx.charts.chartClasses.IAxis#getLabels()
	 */
	public function getLabels(minimumAxisLength:Number):AxisLabelSet
	{
		update();

		return _labelSet;
	}

	/**
     *  @copy mx.charts.chartClasses.IAxis#reduceLabels()
	 */
	public function reduceLabels(intervalStart:AxisLabel,intervalEnd:AxisLabel):AxisLabelSet
	{
		var skipCount:int = _catMap[intervalEnd.value] - _catMap[intervalStart.value] + 1;
		
		if (skipCount <= 0)
			return null;
			
		var newLabels:Array = [];
		var newTicks:Array = [];
		
		var min:Number = -_padding;
		var max:Number = _categoryValues.length - 1 + _padding;
		var alen:Number = (max-min);

		for (var i:int = 0; i < _categoryValues.length; i += skipCount)
		{
			newLabels.push(_labelsMatchToCategoryValuesByIndex[i]);
			newTicks.push(_labelsMatchToCategoryValuesByIndex[i].position);
		}
		
		var axisLabelSet:AxisLabelSet = new AxisLabelSet();
		axisLabelSet.labels = newLabels;
		axisLabelSet.minorTicks = minorTicks;
		axisLabelSet.ticks = generateTicks();
		return axisLabelSet;
	}

	/**
     *  @copy mx.charts.chartClasses.IAxis#update()
	 */
	public function update():void
	{
		if (!_labelSet)
		{
			var prop:Object;
			
			_catMap = {};
			_categoryValues = [];
			_labelsMatchToCategoryValuesByIndex = [];

			var categoryItems:Array = [];
			var i:int;
			
			if (_categoryField == "")
			{
				_cursor.seek(CursorBookmark.FIRST);
				i = 0;
				while (!_cursor.afterLast)
				{
					prop = _cursor.current;
					if (prop != null)
						_catMap[prop.toString()] = i;					
					_categoryValues[i] = categoryItems[i] = prop;
					_cursor.moveNext();
					i++;
				}
			}
			else
			{
				_cursor.seek(CursorBookmark.FIRST);
				i = 0;
				while (!_cursor.afterLast)
				{
					categoryItems[i] = _cursor.current;
					if (categoryItems[i] && _categoryField in categoryItems[i])
					{
						prop = categoryItems[i][_categoryField];
						if (prop != null)
							_catMap[prop.toString()] = i;					
						_categoryValues[i] = prop;
					}
					else
					{
						_categoryValues[i] = null;
					}
					i++;
					_cursor.moveNext()
				}
			}

			var axisLabels:Array = [];
			
			var min:Number = -_padding;
			var max:Number = _categoryValues.length - 1 + _padding;
			var alen:Number = max - min;
			var label:AxisLabel;

			var n:int = _categoryValues.length;
			if (_labelFunction != null)
			{
				var previousValue:Object = null;
				for (i = 0; i < n; i++)
				{
					if (!_categoryValues[i])
						continue;
						
					label = new AxisLabel((i - min) / alen, _categoryValues[i],
						_labelFunction(_categoryValues[i], previousValue,
						this, categoryItems[i]));
					_labelsMatchToCategoryValuesByIndex[i] = label;
					axisLabels.push(label);

					previousValue = _categoryValues[i];
				}
			}
			else
			{
				for (i = 0; i < n; i++)
				{
					if (!_categoryValues[i])
						continue;
					
					label = new AxisLabel((i - min) / alen, _categoryValues[i],
						_categoryValues[i].toString());
					_labelsMatchToCategoryValuesByIndex[i] = label;
					axisLabels.push(label);
				}				
			}

			_labelSet = new AxisLabelSet();
			_labelSet.labels = axisLabels;
			_labelSet.accurate = true;
			_labelSet.minorTicks = minorTicks;
			_labelSet.ticks = generateTicks();			
		}
	}

	/**
	 *  @private
	 */
	private function generateTicks():Array
	{
		if (!_cachedTicks)
		{
			_cachedTicks = [];

			var n:int;
			var min:Number;
			var max:Number;
			var alen:Number;
			var i:Number;
			
			if (_ticksBetweenLabels == false)
			{
				n = _categoryValues.length;
				min = -_padding;
				max = n - 1 + _padding;
				alen = max - min;
				
				for (i = 0; i < n; i++) // <= to draw final tick
				{
					_cachedTicks.push((i - min) / alen);
				}
			}
			else
			{
				_cachedMinorTicks = [];
				
				n = _categoryValues.length;
				min = -_padding;
				max = n - 1 + _padding;
				alen = max - min;
				
				var start:Number = _padding < 0.5 ? 0.5 : -0.5;
				var end:Number = _padding < 0.5 ? n - 1.5 : n - 0.5;
				
				for (i = start; i <= end; i += 1)
				{
					_cachedTicks.push((i - min) / alen);
				}
			}
		}

		return _cachedTicks;
	}
	
	/**
	 *  @private
	 */
	private function invalidateCategories():void
	{
		_labelSet = null;
		_cachedMinorTicks = null;
		_cachedTicks = null;

		dispatchEvent(new Event("mappingChange"));
		dispatchEvent(new Event("axisChange"));
	}
	
	//--------------------------------------------------------------------------
	//
	//  Event handlers
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private function collectionChangeHandler(event:CollectionEvent = null):void
	{
		if (event && event.kind == CollectionEventKind.RESET)
			_cursor = _dataProvider.createCursor();

		invalidateCategories();
	}
	
}

}
